Skip to content

feat(listen): expose --cli-key, adopt its project, and save it when unauthenticated - #329

Merged
leggetter merged 4 commits into
mainfrom
feat/listen-cli-key-flag
Aug 7, 2026
Merged

feat(listen): expose --cli-key, adopt its project, and save it when unauthenticated#329
leggetter merged 4 commits into
mainfrom
feat/listen-cli-key-flag

Conversation

@leggetter

Copy link
Copy Markdown
Collaborator

The Hookdeck Console hands people a ready-to-run command:

npx hookdeck-cli listen [port] <source> --cli-key <key>

Three things made that worse than it needed to be. New Console docs (hookdeck/website#727) document this flow, which is how they surfaced.

1. The flag was invisible

--cli-key is declared on the root command as "(deprecated)" and MarkHidden, so it appears in no help output — not hookdeck --help, not hookdeck listen --help — even though listen's own long help documents it with a worked example, and the Console API's cmd_hint tells users to run exactly that.

Anything introspecting the CLI concludes the flag doesn't exist. The website's docs verifier does precisely that and rejected the documented command:

Unknown flag '--cli-key' for 'hookdeck listen'

Now declared on listen, bound to the same Config field. No behaviour change; it just becomes discoverable. The root flag stays hidden and deprecated.

2. A supplied key was used with the wrong project

apiclient.go sets apiClient.ProjectID = c.Profile.ProjectId. After InitConfig, APIKey holds the flag's value while ProjectId still holds the project from a previous login. They belong to different projects, so every call failed:

Authentication failed: your API key is invalid or expired.

Anyone who had ever logged in and then pasted a Console command hit this, with nothing pointing at the cause. Reproducible on unpatched main.

Client.clientForCLIAuthValidate() already sends ProjectID: "", so validation is project-agnostic and its response carries the key's real project. Adopting that replaces the stale one for the run.

3. The key was forgotten immediately

It applied to one invocation, so every later run needed it pasted again. listen now saves it when the machine has no stored credential, after which the flag can be dropped.

It deliberately does not save when a credential already exists — forwarding a Console source for a few minutes shouldn't silently replace someone's login. In that case the key applies to the run only. Validation happens before any write, so a typo fails with a clear error rather than being persisted.

Config.HasStoredAPIKey records whether a key was on disk before InitConfig folds in the flag value, which the coalesced Profile.APIKey can no longer distinguish.

Worth flagging

This introduces an asymmetry. --api-key passed as a flag does not persist (verified: listen --api-key, gateway ... --api-key with both valid and invalid keys). After this, listen --cli-key does. That reads as intentional to me — --cli-key means "this is my identity, set me up", --api-key means "use this for this call" — and it reinforces the user-scoped vs project-scoped distinction listen's help already draws. But it is a divergence between two flags currently bound to the same variable, and worth a second opinion.

Overlaps with feat/cli-guest-tracking-fixes. That branch removes the root --cli-key entirely and declares one on login only, which would break listen --cli-key — the command the Console tells people to run. Its ConfigureFromClaimedCliKey is close to what this adds and the two should converge; this is the smaller change that unblocks the docs now.

Test plan

  • go build ./..., go vet ./... (one pre-existing warning in pkg/login, present on main)
  • go test ./... — 14 packages pass
  • New tests use the httptest pattern from gateway_test.go, no network
  • Mutation check: removing the project-adoption line makes the test fail with the stale project id
  • Manually verified against the live Console API: first run saves; second run with a different key authenticates and leaves the stored login intact; invalid key writes nothing
  • Release needed before hookdeck/website#727's verify-code-examples goes green — its verifier runs the published binary

🤖 Generated with Claude Code

https://claude.ai/code/session_01HHJQ1QSdKmJMivqw7SER6t

leggetter and others added 3 commits August 7, 2026 15:38
The Hookdeck Console hands users a ready-to-run command:

  npx hookdeck-cli listen [port] <source> --cli-key <key>

Two things made that worse than it needed to be.

The flag was invisible. --cli-key is declared on the root command as
"(deprecated)" and MarkHidden, so it appears in no help output, even
though listen's own help documents it with an example. Anything that
introspects the CLI concludes the flag does not exist. Declaring it on
listen, bound to the same Config field, changes no behaviour and makes
it discoverable in `hookdeck listen --help`.

The key was also forgotten immediately. It applied to one invocation, so
every later run needed it pasted again. listen now validates and saves
the key when the machine has no stored credential, after which the flag
can be dropped.

It deliberately does not save when a credential already exists.
Forwarding a Console source for a few minutes should not silently
replace an existing login, so in that case the key applies to the run
only. Validation happens before the write, so a typo fails with a clear
error instead of being persisted.

Config.HasStoredAPIKey records whether a key was on disk before
InitConfig folds in the flag value, which the coalesced Profile.APIKey
can no longer distinguish.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHJQ1QSdKmJMivqw7SER6t
A key given on the command line was sent alongside the project id read
from the config file. Those belong to different logins, so anyone with an
existing profile who ran the command the Hookdeck Console gives them got
"your API key is invalid or expired" and no way to see why.

Validation is already project-agnostic (Client.clientForCLIAuthValidate
sends an empty ProjectID), so it resolves the project the key really
belongs to. Adopting that response replaces the stale project for the
run. Saving stays conditional on there being no stored credential, so an
existing login is still never replaced.

Renames the helper to applyCliKey, since it now always resolves context
and only sometimes saves.

Tests cover the four branches without touching the network, using the
httptest pattern from gateway_test.go: the flag-absent no-op (pointed at
a dead port, so a stray validate call fails loudly), adopting the key's
project over a stale one, leaving an existing login on disk, and
refusing to write a key that fails validation. A mutation check confirms
the project-adoption test fails when the fix is removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHJQ1QSdKmJMivqw7SER6t

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the hookdeck listen “Console copy/paste” flow by making --cli-key discoverable on the listen command, ensuring the key’s project context is adopted (avoiding stale project_id from prior logins), and persisting the key only when the machine has no stored credentials yet.

Changes:

  • Add a visible --cli-key flag to hookdeck listen while keeping the root flag hidden/deprecated.
  • Resolve and adopt the project context returned by /cli-auth/validate when --cli-key is supplied, preventing mismatched key/project headers.
  • Track whether an API key existed on disk pre-coalesce (HasStoredAPIKey) and persist --cli-key only for first-time (unauthenticated) setups, with unit tests for both behaviors.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
pkg/config/config.go Adds HasStoredAPIKey to distinguish stored credentials from flag-supplied keys during InitConfig coalescing.
pkg/config/has_stored_api_key_test.go Unit tests validating HasStoredAPIKey behavior across stored vs flag-supplied key scenarios.
pkg/cmd/listen.go Declares --cli-key on listen and adds applyCliKey to validate/adopt project context and optionally persist credentials.
pkg/cmd/listen_cli_key_test.go Unit tests covering project adoption, persistence guard behavior, and flag visibility on listen.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/cmd/listen.go Outdated
`--cli-key=` satisfies cobra's Changed check but carries nothing to
authenticate with. The empty value then fell through InitConfig's
coalesce and the run failed with "your API key is invalid or expired",
which describes neither what happened nor how to fix it, after a
pointless round-trip to the API.

Fail immediately with an error naming the flag. Reads the flag value
rather than Profile.APIKey, which by that point may hold a stored key
from the config file instead of what the user typed.

Raised by Copilot review on #329.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHJQ1QSdKmJMivqw7SER6t
@leggetter
leggetter merged commit ad6b2fd into main Aug 7, 2026
12 checks passed
@leggetter
leggetter deleted the feat/listen-cli-key-flag branch August 7, 2026 15:47
@leggetter
leggetter requested a lite review from Copilot August 7, 2026 15:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants